package com.pms.utils;

import java.util.Calendar;
import java.util.Date;

/**
 * The TimeSpan class represents a length of time, measured to the millisecond.
 */
public class TimeSpan {
	/**
	 */
	public final static int YEAR = -2;

	public final static int MONTH = -1;

	public final static int WEEK = 0;

	public final static int DAY = 1;

	public final static int HOUR = 2;

	public final static int MINUTE = 3;

	public final static int SECOND = 4;

	public final static int MILLI = 5;

	public final static String[] cSTRING = { "WEEK", "DAY", "HOUR", "MINUTE",
			"SECOND", "MILLI" };

	// These constants represent then length of each of
	// the following time spans in milliseconds
	public final static long MILLIMILLI = 1;

	public final static long MILLISECOND = 1000 * MILLIMILLI;

	public final static long MILLIMINUTE = 60 * MILLISECOND;

	public final static long MILLIHOUR = 60 * MILLIMINUTE;

	public final static long MILLIDAY = 24 * MILLIHOUR;

	public final static long MILLIWEEK = 7 * MILLIDAY;

	public final static long[] cMILLI = { MILLIWEEK, MILLIDAY, MILLIHOUR,
			MILLIMINUTE, MILLISECOND, MILLIMILLI };

	// These constants represent the maximum value a field can have
	public final static long MAXMILLI = 1000;

	public final static long MAXSECOND = 60;

	public final static long MAXMINUTE = 60;

	public final static long MAXHOUR = 24;

	public final static long MAXDAY = 7;

	public final static long MAXWEEK = 2000000;

	public final static long[] cMAX = { MAXWEEK, MAXDAY, MAXHOUR, MAXMINUTE,
			MAXSECOND, MAXMILLI };

	// these constants are used for overflow checking
	private final static int throwNone = 0;

	private final static int throwPositive = 1;

	private final static int throwNegative = 2;

	// /////////////////////////////////////////
	// CONSTRUCTORS

	// CONSTRUCTOR: Sets all fields to cleared
	public TimeSpan() {
		setStartCalendar(null);
		set(0);
	}

	public TimeSpan(Calendar aCalendar) {
		set(aCalendar, 0);
	}

	// CONSTRUCTOR: Sets the internal millseconds representation of the time
	// span
	public TimeSpan(long aMillis) {
		setStartCalendar(null);
		set(aMillis);
	}

	public TimeSpan(Calendar aCalendar, long aMillis) {
		set(aCalendar, aMillis);
	}

	// CONSTRUCTOR: Copies another timespan into this one
	public TimeSpan(TimeSpan aTimeSpan) {
		setStartCalendar(null);
		set(aTimeSpan);
	}

	public TimeSpan(Calendar aCalendar, TimeSpan aTimeSpan) {
		set(aCalendar, aTimeSpan);
	}

	// CONSTRUCTOR: Sets the timespan time
	public TimeSpan(long aWeeks, long aDays, long aHours, long aMinutes,
			long aSeconds, long aMillis) {
		setStartCalendar(null);
		set(aWeeks, aDays, aHours, aMinutes, aSeconds, aMillis);
	}

	public TimeSpan(Calendar aCalendar, long aWeeks, long aDays, long aHours,
			long aMinutes, long aSeconds, long aMillis) {
		set(aCalendar, aWeeks, aDays, aHours, aMinutes, aSeconds, aMillis);
	}

	// CONSTRUCTOR: Sets the timespan to the difference between aCal1 and aCal2
	public TimeSpan(Calendar aCal1, Calendar aCal2) {
		setStartCalendar(null);
		set(aCal1, aCal2);
	}

	public TimeSpan(Calendar aCalendar, Calendar aCal1, Calendar aCal2) {
		set(aCalendar, aCal1, aCal2);
	}

	// Works out the difference between aCal1 and aCal2 and returns the timespan
	// object(this)
	// for any further operations
	// * throws exceptions if any of the calendars are null
	public void set(Calendar aCal1, Calendar aCal2) {
		setTime(aCal2.getTime().getTime() - aCal1.getTime().getTime());
	}

	public void set(Calendar aCalendar, Calendar aCal1, Calendar aCal2) {
		setStartCalendar(aCalendar);
		set(aCal1, aCal2);
	}

	// Sets the timespan to the value of another timespan
	public void set(TimeSpan aTimeSpan) {
		setStartCalendar(aTimeSpan.getStartCalendar());
		setTime(aTimeSpan.getTime());
	}

	public void set(Calendar aCalendar, TimeSpan aTimeSpan) {
		setStartCalendar(aCalendar);
		setTime(aTimeSpan.getTime());
	}

	// Sets the timespans internal representation of time
	public void set(Calendar aCalendar, long aMillis) {
		setStartCalendar(aCalendar);
		setTime(aMillis);
	}

	public void set(long aMillis) {
		setTime(aMillis);
	}

	// Sets this timespan to the values passes, any values that exceed there
	// normal amounts i.e. 67seconds are just passed up the chain, i.e. becomes
	// 1minute 7seconds
	public void set(long aWeeks, long aDays, long aHours, long aMinutes,
			long aSeconds, long aMillis) {
		setTime(0);

		addToField(WEEK, aWeeks);
		addToField(DAY, aDays);
		addToField(HOUR, aHours);
		addToField(MINUTE, aMinutes);
		addToField(SECOND, aSeconds);
		addToField(MILLI, aMillis);
	}

	public void set(Calendar aCalendar, long aWeeks, long aDays, long aHours,
			long aMinutes, long aSeconds, long aMillis) {
		setStartCalendar(aCalendar);
		set(aWeeks, aDays, aHours, aMinutes, aSeconds, aMillis);
	}

	// gets a field and all fields above it and returns it in aUnit units
	public long getFieldAccum(int aField, int aUnit) {
		long answer = (long) (getTime() / cMILLI[aField]); // get the time in
															// terms of aField
															// units(rounded)

		return (long) (answer * cMILLI[aField] / cMILLI[aUnit]); // return
																	// the time
																	// in terms
																	// of aUnit
																	// units
	}

	public long getFieldAccum(int aField) {
		return getFieldAccum(aField, aField);
	}

	// get a field not including the fields above it and returns it in aUnit
	// units
	public long getField(int aField, int aUnit) {
		long returnValue = (long) (getFieldAccum(aField) % cMAX[aField] * cMILLI[aField]);

		returnValue /= cMILLI[aUnit];

		return returnValue;
	}

	public long getField(int aField) {
		return getField(aField, aField);
	}

	// adds an amount of time to a field, i.e. add 6 to days
	public void addToField(int aField, long aValue) {
		int throwTest = initThrowOverflowProduct(aValue, getTime());

		long product = aValue * cMILLI[aField];

		checkThrowOverflow(throwTest, product);

		add(product);
	}

	private void add(long aValue) {
		int throwTest = initThrowOverflowSum(aValue, getTime());

		setTime(getTime() + aValue);

		checkThrowOverflow(throwTest, getTime());
	}

	// adds a TimeSpan to this TimeSpan
	public TimeSpan add(TimeSpan aValue) {
		add(aValue.getTime());

		return this;
	}

	// subtracts a TimeSpan from this TimeSpan
	public TimeSpan subtract(TimeSpan aValue) {
		add(-aValue.getTime());

		return this;
	}

	// checks whether two time spans are equal, using aUnits as the minimum size
	// to compare to
	// i.e. I want to compare to times to see if they are on the same day, pass
	// DAYS for aUnits
	public boolean equalTo(TimeSpan aTimeSpan, int aUnits) {
		return (getFieldAccum(MILLI, aUnits) == aTimeSpan.getFieldAccum(MILLI,
				aUnits));
	}

	// checks whether this time spans is less than the passed one, using aUnits
	// as the minimum size to compare to
	public boolean lessThan(TimeSpan aTimeSpan, int aUnits) {
		return (getFieldAccum(MILLI, aUnits) < aTimeSpan.getFieldAccum(MILLI,
				aUnits));
	}

	// checks whether this time spans is greater than the passed one, using
	// aUnits as the minimum size to compare to
	public boolean greaterThan(TimeSpan aTimeSpan, int aUnits) {
		return (getFieldAccum(MILLI, aUnits) > aTimeSpan.getFieldAccum(MILLI,
				aUnits));
	}

	// Adds this timespan to a calendar
	public Calendar addToCalendar(Calendar aCalendar) {
		if (aCalendar == null)
			throw (new RuntimeException(
					"TimeSpan::addToCalendar( Calendar aCalendar=null ) - aCalendar is null."));

		aCalendar.setTime(new Date(aCalendar.getTime().getTime() + getTime()));

		return aCalendar;
	}

	public Calendar getStartCalendar(int aUnits) {
		Calendar returnCalendar = (Calendar) getStartCalendar().clone();

		/* = MILLI */
		if (aUnits == MILLI) {
			return returnCalendar;
		} else if (aUnits == SECOND) {
			returnCalendar.set(Calendar.MILLISECOND, 0);

			return returnCalendar;
		} else if (aUnits == MINUTE) {
			returnCalendar.set(Calendar.MILLISECOND, 0);
			returnCalendar.set(Calendar.SECOND, 0);

			return returnCalendar;
		} else if (aUnits == HOUR) {
			returnCalendar.set(Calendar.MILLISECOND, 0);
			returnCalendar.set(Calendar.SECOND, 0);
			returnCalendar.set(Calendar.MINUTE, 0);

			return returnCalendar;
		} else if (aUnits == DAY) {
			returnCalendar.set(Calendar.MILLISECOND, 0);
			returnCalendar.set(Calendar.SECOND, 0);
			returnCalendar.set(Calendar.MINUTE, 0);
			returnCalendar.set(Calendar.HOUR_OF_DAY, 0);

			return returnCalendar;
		} else if (aUnits == WEEK) {
			returnCalendar.set(Calendar.MILLISECOND, 0);
			returnCalendar.set(Calendar.SECOND, 0);
			returnCalendar.set(Calendar.MINUTE, 0);
			returnCalendar.set(Calendar.HOUR_OF_DAY, 0);
			returnCalendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
			return returnCalendar;
		} else if (aUnits == MONTH) {
			returnCalendar.set(Calendar.MILLISECOND, 0);
			returnCalendar.set(Calendar.SECOND, 0);
			returnCalendar.set(Calendar.MINUTE, 0);
			returnCalendar.set(Calendar.HOUR_OF_DAY, 0);
			returnCalendar.set(Calendar.DAY_OF_MONTH, 1);

			return returnCalendar;
		} else if (aUnits == YEAR) {
			returnCalendar.set(Calendar.MILLISECOND, 0);
			returnCalendar.set(Calendar.SECOND, 0);
			returnCalendar.set(Calendar.MINUTE, 0);
			returnCalendar.set(Calendar.HOUR_OF_DAY, 0);
			returnCalendar.set(Calendar.MONTH, 1);
			returnCalendar.set(Calendar.DATE, 1);

			return returnCalendar;
		}

		throw (new RuntimeException("Unknown Field"));
	}

	public Calendar getEndCalendar(int aUnits) {
		return getEndCalendar(aUnits, false);
	}

	public Calendar getEndCalendar(int aUnits, boolean aInclusive) {
		Calendar returnCalendar = (Calendar) getStartCalendar().clone();

		addToCalendar(returnCalendar);

		/* = MILLI */
		if (aUnits == MILLI) {
			returnCalendar.add(Calendar.MILLISECOND, -1);
			return returnCalendar;
		} else if (aUnits == SECOND) {
			returnCalendar.set(Calendar.MILLISECOND, (int) (MAXMILLI - 1));

			return returnCalendar;
		} else if (aUnits == MINUTE) {
			returnCalendar.set(Calendar.MILLISECOND, (int) (MAXMILLI - 1));
			returnCalendar.set(Calendar.SECOND, (int) (MAXSECOND - 1));

			return returnCalendar;
		} else if (aUnits == HOUR) {
			returnCalendar.set(Calendar.MILLISECOND, (int) (MAXMILLI - 1));
			returnCalendar.set(Calendar.SECOND, (int) (MAXSECOND - 1));
			returnCalendar.set(Calendar.MINUTE, (int) (MAXMINUTE - 1));

			return returnCalendar;
		} else if (aUnits == DAY) {
			returnCalendar.set(Calendar.HOUR, (int) (MAXHOUR - 1));
			returnCalendar.set(Calendar.MINUTE, (int) (MAXMINUTE - 1));
			returnCalendar.set(Calendar.SECOND, (int) (MAXSECOND - 1));
			returnCalendar.set(Calendar.MILLISECOND, (int) (MAXMILLI - 1));

			return returnCalendar;
		} else if (aUnits == WEEK) {
			long oldCalendar = returnCalendar.getTime().getTime();

			returnCalendar.set(Calendar.MILLISECOND, (int) (MAXMILLI - 1));
			returnCalendar.set(Calendar.SECOND, (int) (MAXSECOND - 1));
			returnCalendar.set(Calendar.MINUTE, (int) (MAXMINUTE - 1));
			returnCalendar.set(Calendar.HOUR_OF_DAY, (int) (MAXHOUR - 1));
			returnCalendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
			if ((returnCalendar.getTime().getTime() - oldCalendar) < 0)
				returnCalendar.add(Calendar.DATE, 7);

			return returnCalendar;
		} else if (aUnits == MONTH) {
			returnCalendar.set(Calendar.MILLISECOND, (int) (MAXMILLI - 1));
			returnCalendar.set(Calendar.SECOND, (int) (MAXSECOND - 1));
			returnCalendar.set(Calendar.MINUTE, (int) (MAXMINUTE - 1));
			returnCalendar.set(Calendar.HOUR_OF_DAY, (int) (MAXHOUR - 1));
			returnCalendar.set(Calendar.DAY_OF_MONTH, 1);
			returnCalendar.add(Calendar.MONTH, 1);
			returnCalendar.add(Calendar.DATE, -1);

			return returnCalendar;
		} else if (aUnits == YEAR) {
			returnCalendar.set(Calendar.MILLISECOND, (int) (MAXMILLI - 1));
			returnCalendar.set(Calendar.SECOND, (int) (MAXSECOND - 1));
			returnCalendar.set(Calendar.MINUTE, (int) (MAXMINUTE - 1));
			returnCalendar.set(Calendar.HOUR_OF_DAY, (int) (MAXHOUR - 1));
			returnCalendar.set(Calendar.DAY_OF_YEAR, 1);
			returnCalendar.add(Calendar.YEAR, 1);
			returnCalendar.add(Calendar.DATE, -1);

			return returnCalendar;
		}

		throw (new RuntimeException("Unknown Field"));
	}

	// /////////////////////////////
	// ABSTRACTION
	public void setTime(long aTime) {
		mTime = aTime;
	}

	public long getTime() {
		return mTime;
	}

	private void setStartCalendar(Calendar aCalendar) {
		startCalendar = aCalendar;
	}

	private Calendar getStartCalendar() {
		return startCalendar;
	}

	// /////////////////////////////
	// SAFETY CHECKS
	private int initThrowOverflowProduct(long factor1, long factor2) {
		if (((factor1 < 0) && (factor2 < 0)) || (factor1 > 0) && (factor2 > 0))
			return throwNegative;
		if (((factor1 > 0) && (factor2 < 0)) || (factor1 < 0) && (factor2 > 0))
			return throwPositive;
		return throwNone;
	}

	private int initThrowOverflowSum(long addend1, long addend2) {
		if ((addend1 < 0) && (addend2 < 0))
			return throwPositive;
		if ((addend1 > 0) && (addend2 > 0))
			return throwNegative;
		return throwNone;
	}

	private void checkThrowOverflow(int aThrow, long aValue)
			throws RuntimeException {
		if ((aThrow == throwPositive && aValue > 0)
				|| (aThrow == throwNegative && aValue < 0))
			throw (new RuntimeException(
					"TimeSpan::checkThrowOverflow() - Overflow"));

		return;
	}

	// /////////////////////////////
	// DEBUGING CODE
	//

	// Formats this TimeSpan object as a string
	public String toString() {
		String returnString = "TimeSpan=[ mTime=" + getTime() + " WEEK="
				+ getField(WEEK, WEEK) + " DAY=" + getField(DAY, DAY)
				+ " HOUR=" + getField(HOUR, HOUR) + " MINUTE="
				+ getField(MINUTE, MINUTE) + " SECOND="
				+ getField(SECOND, SECOND) + " MILLI=" + getField(MILLI, MILLI)
				+ "]";
		if (startCalendar != null)
			returnString += " " + getStartCalendar().toString();

		return returnString;
	}

	private long mTime = 0;

	private Calendar startCalendar = null;
}